﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Snowdreamist.Threading
{
    public class BackgroundJob
    {
        #region static

        /// <summary>
        /// Time schedule precision
        /// </summary>
        public readonly static TimeSpan TimePrecision = TimeSpan.FromSeconds(30);

        /// <summary>
        ///
        /// </summary>
        /// <param name="action"></param>
        /// <param name="param"></param>
        /// <returns></returns>
        public static BackgroundJob Run(Delegate action, params object[] param)
        {
            return Run(action, TimeSpan.MaxValue, string.Empty, param);
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="action"></param>
        /// <param name="param"></param>
        /// <returns></returns>
        public static BackgroundJob Run(Delegate action, string friendlyName, params object[] param)
        {
            return Run(action, TimeSpan.MaxValue, friendlyName, param);
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="action"></param>
        /// <param name="Interval"></param>
        /// <param name="param"></param>
        /// <returns></returns>
        public static BackgroundJob Run(Delegate action, TimeSpan interval, params object[] param)
        {
            return Run(action, interval, string.Empty, param);
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="action"></param>
        /// <param name="Interval"></param>
        /// <param name="param"></param>
        /// <returns></returns>
        public static BackgroundJob Run(Delegate action, TimeSpan interval, string friendlyName, params object[] param)
        {
            if (action == null)
                throw new ArgumentNullException("action");
            if (interval < TimePrecision)
                throw new ArgumentException("Interval cannot smaller than TimePrecision");

            var job = new BackgroundJob(action)
            {
                Interval = interval,
                Parameters = param,
                NextUTCRuntime = DateTime.UtcNow + interval,
                FriendlyName = friendlyName
            };

            AttachJob(job);

            return job;
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="action"></param>
        /// <param name="Interval"></param>
        /// <param name="MaxTimes"></param>
        /// <param name="param"></param>
        /// <returns></returns>
        public static BackgroundJob Run(Delegate action, TimeSpan interval, int maxTimes, params object[] param)
        {
            if (action == null)
                throw new ArgumentNullException("action");
            if (interval < TimePrecision)
                throw new ArgumentException("Interval cannot smaller than TimePrecision");
            if (maxTimes < 1)
                throw new ArgumentException("Invalid maxTimes");

            var job = new BackgroundJob(action)
            {
                Interval = interval,
                MaxTimes = maxTimes,
                Parameters = param,
                NextUTCRuntime = DateTime.UtcNow + interval
            };

            AttachJob(job);

            return job;
        }

        /// <summary>
        /// List all background jobs
        /// </summary>
        public static IEnumerable<BackgroundJob> Jobs
        {
            get
            {
                return _jobs.ToArray();
            }
        }

        public static event EventHandler<JobCompletedEventArgs> JobCompleted;

        #endregion static

        private BackgroundJob(Delegate action)
        {
            this.Action = action;
            this.MaxTimes = 1;
            this.RanTimes = 0;
            this.State = JobState.Waiting;
            this.Interval = TimeSpan.MaxValue;
        }

        public void Pause()
        {
            if (this.State != JobState.Destroyed)
                this.State = JobState.Pasued;
        }

        public void Resume()
        {
            if (this.State == JobState.Pasued)
            {
                this.NextUTCRuntime = DateTime.UtcNow + this.Interval;
                this.State = JobState.Waiting;
            }
        }

        public void Stop()
        {
            DetachJob(this);
        }

        public string FriendlyName { get; set; }

        public DateTime NextRuntime
        {
            get
            {
                return this.NextUTCRuntime.ToLocalTime();
            }
        }

        public TimeSpan Interval { get; internal set; }

        public int RanTimes { get; internal set; }

        public int MaxTimes { get; internal set; }

        public JobState State
        {
            get
            {
                return this._jobState;
            }
            set
            {
                var oldState = this._jobState;
                this._jobState = value;
                if (oldState != value)
                    this.OnJobStateChanged(oldState, value);
            }
        }

        public event EventHandler<JobCompletedEventArgs> Completed;

        public event EventHandler<JobStateChangedEventArgs> StateChanged;

        #region internal

        /// <summary>
        /// For better performance
        /// </summary>
        internal DateTime NextUTCRuntime { get; set; }

        internal Delegate Action { get; set; }

        internal object[] Parameters { get; set; }

        #endregion internal

        #region protected

        protected void OnCompleted(Exception error)
        {
            if (this.Completed != null)
                this.Completed(this, new JobCompletedEventArgs(this, error));
        }

        protected void OnJobStateChanged(JobState oldState, JobState newState)
        {
            if (this.StateChanged != null)
                this.StateChanged(this, new JobStateChangedEventArgs(oldState, newState));
        }

        #endregion protected

        #region private

        /// <summary>
        /// Job State
        /// </summary>
        private JobState _jobState;
        /// <summary>
        /// Lock object
        /// </summary>
        private readonly static object _syncRoot = new object();
        /// <summary>
        /// Jobs
        /// </summary>
        private readonly static List<BackgroundJob> _jobs = new List<BackgroundJob>();
        /// <summary>
        /// Timer
        /// </summary>
        private static Timer _timer = new Timer(Schedule, null, TimePrecision, TimePrecision);

        private static void Schedule(object state)
        {
            IEnumerable<BackgroundJob> jobs = null;
            DateTime utcNow = DateTime.UtcNow;

            lock (_syncRoot)
                jobs = _jobs.Where(p => p.State == JobState.Waiting && p.NextUTCRuntime < utcNow).ToArray();

            foreach (var job in jobs)
                Task.Factory.StartNew(new Action<object>(JobRunner), job, TaskCreationOptions.LongRunning);
        }

        private static void JobRunner(object arg)
        {
            BackgroundJob job = arg as BackgroundJob;

            if (job == null) return;

            job.State = JobState.Running;

            Exception error = null;

            try
            {
                job.Action.DynamicInvoke(job.Parameters);
            }
            catch (Exception e)
            {
                // error
                error = e;
            }

            OnJobCompleted(job, error);
        }

        private static void OnJobCompleted(BackgroundJob job, Exception error)
        {
            job.RanTimes++;

            if (job.RanTimes >= job.MaxTimes)
            {
                // its done
                job.State = JobState.Destroyed;
                DetachJob(job);
            }
            else
            {
                job.State = JobState.Waiting;
                job.NextUTCRuntime = DateTime.UtcNow + job.Interval;
            }

            // notify
            if (JobCompleted != null)
                JobCompleted(job, new JobCompletedEventArgs(job, error));
            job.OnCompleted(error);
        }

        private static void AttachJob(BackgroundJob job)
        {
            lock (_syncRoot)
            {
                if (!_jobs.Contains(job))
                    _jobs.Add(job);
            }
        }

        private static void DetachJob(BackgroundJob job)
        {
            lock (_syncRoot)
            {
                if (_jobs.Contains(job))
                    _jobs.Remove(job);
            }
        }

        #endregion private
    }
}